package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 聊天室服务端
 */
public class Server {
    /**
     * java.net.ServerSocket
     * 运行在服务端的ServerSocket主要的作用
     * 1:向系统申请端口，用于让客户端可以通过该端口与之建立连接
     * 2:监听端口，等待客户端连接，一旦客户端连接则会返回一个Socket实例
     *   使用该Socket实例即可与客户端进行双向交互
     *
     * 如果将Socket比喻为电话，那么ServerSocket就是"总机"
     */
    private ServerSocket serverSocket;

    //线程池
    private ExecutorService threadPool = Executors.newFixedThreadPool(30);

    //存放所有客户端的输出流，用于广播消息
//    private List<PrintWriter> allOut = new ArrayList<>();
    //可以将allOut定义为并发安全的集合
//    private List<PrintWriter> allOut = Collections.synchronizedList(new ArrayList<>());
    /*
        ConcurrentHashMap是JUC包中提供的一个并发安全的HashMap

        java.util.HashMap和LinkedHashMap都不是并发安全的
     */
    //key:用户昵称  value:该客户端对应的输出流
    private Map<String,PrintWriter> allOut = new ConcurrentHashMap<>();

    public Server(){
        try {
            System.out.println("正在启动服务端...");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start(){
        try {
            while(true) {
                System.out.println("等待客户端连接...");
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了!");
                //启动一个线程与该客户端交互
                ClientHandler handler = new ClientHandler(socket);
//                Thread t = new Thread(handler);
//                t.start();
                threadPool.execute(handler);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * 该线程任务负责与指定客户端交互
     */
    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;//记录客户端的IP地址
        private String nickname;//客户端昵称

        public ClientHandler(Socket socket){
            this.socket = socket;
            //通过socket获取远端计算机地址信息
            host = socket.getInetAddress().getHostAddress();
        }

        public void run(){
            PrintWriter pw = null;
            try {
                /*
                    Socket提供的方法:
                    InputStream getInputStream()
                    获取一个字节输入流，用于读取远端计算机发送过来的数据
                 */
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                /*
                    通过socket获取输出流，用于给客户端发送消息
                 */
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);

//                synchronized (allOut) {
//                    //将该输出流存入共享集合allOut中，便于其他线程访问到，将消息广播给该客户端
//                    allOut.add(pw);
//                }

                //如果allOut为并发安全的集合，则不需要单独加锁
//                allOut.add(pw);


                //首先读取客户端发送过来的第一行字符串(昵称)
                nickname = br.readLine();

                //读取到昵称后再将输出流存入共享Map
                allOut.put(nickname,pw);

                //广播消息，通知该用户上线了
                sendMessage(nickname+"上线了，当前在线人数:"+ allOut.size());

                String message;
                /*
                    br.readLine()读取来自远端计算机发送过来的一行字符串时
                    该方法会进入阻塞状态，直到对方发送过来了一行字符串为止，此时方法会将这行
                    内容进行返回。

                    当客户端断开链接时，服务端这里的readLine会出现几种情况
                    客户端异常断开:客户端没有关闭链接，会导致服务端这边readLine方法抛出异常
                    java.net.SocketException: Connection reset

                    客户端正常断开:客户端调用了socket的close操作，此时服务端这里readLine
                    方法会返回null。
                 */
                while ((message = br.readLine()) != null) {
                    //如果聊天消息是以"@"开始则认为是私聊功能
                    if(message.startsWith("@")){
                        sendMessageToSomeone(message);
                    }else {
                        sendMessage(nickname + "(" + host + ")说:" + message);
                        //将群聊消息保存到数据库
                        saveMessage(nickname,null,message);
                    }
                }
            } catch (IOException e) {
                //当客户端异常端口，不做任何操作
            } finally {
                //处理客户端断开后的操作
                //1 将该客户端的输出流从共享集合中删除
//                synchronized (allOut) {
//                    allOut.remove(pw);
//                }
                //并发安全的集合不需要单独加锁
//                allOut.remove(pw);
                allOut.remove(nickname);

                //2 通知其他客户端，该用户下线了
                sendMessage(nickname+"下线了，当前在线人数:"+allOut.size());
                //3 将socket.close()释放底层资源
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 广播消息给所有客户端
         * @param message
         */
        private void sendMessage(String message){
            System.out.println(message);
//            synchronized (allOut) {
//                for (PrintWriter o : allOut) {
//                    o.println(message);
//                }
//            }
            //新循环就是迭代器，迭代器不会与集合方法互斥的，哪怕集合是并发安全的集合
//            for (PrintWriter o : allOut) {
//                o.println(message);
//            }

            //对于并发安全的集合而言，应当选取forEach方法遍历，才能与集合其他操作互斥
//            allOut.forEach(e->e.println(message));

            allOut.values().forEach(e->e.println(message));
        }

        /**
         * 私聊功能，将消息转发给指定用户
         * @param message
         */
        private void sendMessageToSomeone(String message){
            //message->@王克晶:在吗?
            //验证私聊格式:
            if(message.matches("@.+:.+")){
                //从聊天消息中截取出对方昵称
                String toNickname = message.substring(1,message.indexOf(":"));
                if(allOut.containsKey(toNickname)) {//对方存在
                    //截取出私聊内容
                    String toMessage = message.substring(message.indexOf(":") + 1);
                    toMessage = nickname + "悄悄对你说:" + toMessage;
                    allOut.get(toNickname).println(toMessage);//将消息发送给对方
                    allOut.get(nickname).println("消息已发送给" + toNickname);//通知本用户私聊已发送
                    //将私聊消息保存到数据库
                    saveMessage(nickname,toNickname,toMessage);

                }else{//对方不存在
                    allOut.get(nickname).println("用户["+toNickname+"]不存在!");
                }
            }else{
                //通知本用户，私聊格式不对
                allOut.get(nickname).println("私聊格式错误，正确格式[@昵称:聊天信息]");
            }
        }

        /**
         * 将聊条信息保存到数据库
         * @param fromUser  发送人
         * @param toUser    接收人
         * @param message   聊天消息
         */
        private void saveMessage(String fromUser,String toUser,String message){
            try (Connection connection = DBUtil.getConnection()) {
                String sql = "INSERT INTO chatinfo(from_user,to_user,content) " +
                             "VALUES(?,?,?)";
                PreparedStatement ps = connection.prepareStatement(sql);
                ps.setString(1,fromUser);
                ps.setString(2,toUser);
                ps.setString(3,message);
                ps.executeUpdate();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }


    }

}







